/**
 * The contents of this file are subject to the OpenMRS Public License
 * Version 1.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://license.openmrs.org
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations
 * under the License.
 *
 * Copyright (C) OpenMRS, LLC.  All Rights Reserved.
 */
package org.openmrs.api.db.hibernate;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.HibernateException;
import org.hibernate.Interceptor;
import org.hibernate.cfg.Configuration;
import org.hibernate.util.ConfigHelper;
import org.openmrs.api.context.Context;
import org.openmrs.module.Module;
import org.openmrs.module.ModuleFactory;
import org.openmrs.util.OpenmrsUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean;

public class HibernateSessionFactoryBean extends AnnotationSessionFactoryBean {
	
	private static Log log = LogFactory.getLog(HibernateSessionFactoryBean.class);
	
	protected Set<String> tmpMappingResources = new HashSet<String>();
	
	/**
	 * @since 1.9.2, 1.10
	 */
	protected Set<String> packagesToScan = new HashSet<String>();
	
	// @since 1.6.3, 1.7.2, 1.8.0, 1.9
	protected ChainingInterceptor chainingInterceptor = new ChainingInterceptor();
	
	// @since 1.6.3, 1.7.2, 1.8.0, 1.9
	// This will be sorted on keys before being used
	@Autowired(required = false)
	public Map<String, Interceptor> interceptors = new HashMap<String, Interceptor>();
	
	//public SessionFactory newSessionFactory(Configuration config) throws HibernateException {
	public Configuration newConfiguration() throws HibernateException {
		Configuration config = super.newConfiguration();
		
		log.debug("Configuring hibernate sessionFactory properties");
		
		Properties moduleProperties = Context.getConfigProperties();
		
		// override or initialize config properties with module-provided ones
		for (Object key : moduleProperties.keySet()) {
			String prop = (String) key;
			String value = (String) moduleProperties.get(key);
			log.trace("Setting module property: " + prop + ":" + value);
			config.setProperty(prop, value);
			if (!prop.startsWith("hibernate"))
				config.setProperty("hibernate." + prop, value);
		}
		
		Properties properties = Context.getRuntimeProperties();
		
		// loop over runtime properties and override each in the configuration
		for (Object key : properties.keySet()) {
			String prop = (String) key;
			String value = (String) properties.get(key);
			log.trace("Setting property: " + prop + ":" + value);
			config.setProperty(prop, value);
			if (!prop.startsWith("hibernate"))
				config.setProperty("hibernate." + prop, value);
		}
		
		// load in the default hibernate properties
		try {
			InputStream propertyStream = ConfigHelper.getResourceAsStream("/hibernate.default.properties");
			Properties props = new Properties();
			OpenmrsUtil.loadProperties(props, propertyStream);
			propertyStream.close();
			
			// Only load in the default properties if they don't exist
			config.mergeProperties(props);
		}
		catch (IOException e) {
			log.fatal("Unable to load default hibernate properties", e);
		}
		
		log.debug("Setting global Hibernate Session Interceptor for SessionFactory, Interceptor: " + chainingInterceptor);
		
		// make sure all autowired interceptors are put onto our chaining interceptor
		// sort on the keys so that the devs/modules have some sort of control over the order of the interceptors 
		List<String> keys = new ArrayList<String>(interceptors.keySet());
		Collections.sort(keys);
		for (String key : keys) {
			chainingInterceptor.addInterceptor(interceptors.get(key));
		}
		
		config.setInterceptor(chainingInterceptor);
		
		return config;
	}
	
	/**
	 * Collect the mapping resources for future use because the mappingResources object is defined
	 * as 'private' instead of 'protected'
	 * 
	 * @see org.springframework.orm.hibernate3.LocalSessionFactoryBean#setMappingResources(java.lang.String[])
	 */
	@Override
	public void setMappingResources(String[] mappingResources) {
		for (String resource : mappingResources) {
			tmpMappingResources.add(resource);
		}
		
		super.setMappingResources(tmpMappingResources.toArray(new String[] {}));
	}
	
	/**
	 * Collect packages to scan that are set in core and for tests in modules.
	 * <p>
	 * It adds to the set instead of overwriting it with each call.
	 * 
	 * @see org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean#setPackagesToScan(java.lang.String[])
	 */
	@Override
	public void setPackagesToScan(String[] packagesToScan) {
		this.packagesToScan.addAll(Arrays.asList(packagesToScan));
		
		super.setPackagesToScan(this.packagesToScan.toArray(new String[0]));
	}
	
	public Set<String> getModuleMappingResources() {
		for (Module mod : ModuleFactory.getStartedModules()) {
			for (String s : mod.getMappingFiles()) {
				tmpMappingResources.add(s);
			}
		}
		return tmpMappingResources;
	}
	
	/**
	 * Gets packages with mapped classes from all modules.
	 * 
	 * @return the set of packages with mapped classes
	 * @since 1.9.2, 1.10
	 */
	public Set<String> getModulePackagesWithMappedClasses() {
		Set<String> packages = new HashSet<String>();
		for (Module module : ModuleFactory.getStartedModules()) {
			for (String pack : module.getPackagesWithMappedClasses()) {
				packages.add(pack);
			}
		}
		return packages;
	}
	
	/**
	 * Overridden to populate mappings from modules.
	 * 
	 * @see org.springframework.orm.hibernate3.AbstractSessionFactoryBean#afterPropertiesSet()
	 */
	@Override
	public void afterPropertiesSet() throws Exception {
		// adding each module's mapping file to the list of mapping resources
		super.setMappingResources(getModuleMappingResources().toArray(new String[] {}));
		
		packagesToScan.addAll(getModulePackagesWithMappedClasses());
		super.setPackagesToScan(packagesToScan.toArray(new String[0]));
		
		super.afterPropertiesSet();
	}
	
	/**
	 * @see org.springframework.orm.hibernate3.LocalSessionFactoryBean#destroy()
	 */
	@Override
	public void destroy() throws HibernateException {
		try {
			super.destroy();
		}
		catch (IllegalStateException e) {
			// ignore errors sometimes thrown by the CacheManager trying to shut down twice
			// see net.sf.ehcache.CacheManager#removeShutdownHook()
		}
	}
	
	/**
	 * Used by the module testing framework to set the dependent modules in the hibernate session
	 * factory
	 * 
	 * @see org.springframework.orm.hibernate3.LocalSessionFactoryBean#setMappingJarLocations(org.springframework.core.io.Resource[])
	 */
	@Override
	public void setMappingJarLocations(Resource[] mappingJarLocations) {
		super.setMappingJarLocations(mappingJarLocations);
	}
	
}
